#!/bin/bash
# /usr/lib/check_mk_agent/local/300/check_pm2

set -u

CONFIG="/etc/check_mk/pm2.conf"

OK=0
WARN=1
CRIT=2
UNKNOWN=3

#
# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------
#

if [[ ! -r "$CONFIG" ]]; then
    printf '%s\n' "${UNKNOWN} \"PM2\" - Configuration file ${CONFIG} missing or unreadable"
    exit 0
fi

# shellcheck disable=SC1090
source "$CONFIG"

PM2_USER="${PM2_USER:-root}"
PM2_HOME="${PM2_HOME:-}"

CHECK_PM2_UPDATES="${CHECK_PM2_UPDATES:-yes}"
CHECK_NODE_UPDATES="${CHECK_NODE_UPDATES:-yes}"

PM2_PACKAGE_URL="${PM2_PACKAGE_URL:-https://registry.npmjs.org/pm2/latest}"
NODE_RELEASES_URL="${NODE_RELEASES_URL:-https://nodejs.org/dist/index.json}"

#
# ---------------------------------------------------------------------------
# Requirements
# ---------------------------------------------------------------------------
#

if ! command -v jq >/dev/null 2>&1; then
    printf '%s\n' "${UNKNOWN} \"PM2\" - jq is not installed"
    exit 0
fi

if ! command -v curl >/dev/null 2>&1; then
    printf '%s\n' "${UNKNOWN} \"PM2\" - curl is not installed"
    exit 0
fi

if ! id "$PM2_USER" >/dev/null 2>&1; then
    printf '%s\n' "${UNKNOWN} \"PM2\" - Configured PM2 user ${PM2_USER} does not exist"
    exit 0
fi

#
# ---------------------------------------------------------------------------
# User / PM2 environment
# ---------------------------------------------------------------------------
#

USER_HOME="$(
    getent passwd "$PM2_USER" |
        cut -d: -f6
)"

if [[ -z "$USER_HOME" ]]; then
    printf '%s\n' "${UNKNOWN} \"PM2\" - Unable to determine home directory for ${PM2_USER}"
    exit 0
fi

if [[ -z "$PM2_HOME" ]]; then
    PM2_HOME="${USER_HOME}/.pm2"
fi

#
# ---------------------------------------------------------------------------
# Locate PM2
# ---------------------------------------------------------------------------
#

PM2_BIN=""

for candidate in \
    /usr/bin/pm2 \
    /usr/local/bin/pm2 \
    "${USER_HOME}/.local/bin/pm2"
do
    if [[ -x "$candidate" ]]; then
        PM2_BIN="$candidate"
        break
    fi
done

if [[ -z "$PM2_BIN" ]]; then
    PM2_BIN="$(command -v pm2 2>/dev/null || true)"
fi

#
# NVM fallback
#

if [[ -z "$PM2_BIN" && -d "${USER_HOME}/.nvm/versions/node" ]]; then
    PM2_BIN="$(
        find "${USER_HOME}/.nvm/versions/node" \
            -mindepth 3 \
            -maxdepth 3 \
            -type f \
            -path '*/bin/pm2' \
            -perm -u+x \
            2>/dev/null |
            sort -V |
            tail -n 1
    )"
fi

if [[ -z "$PM2_BIN" || ! -x "$PM2_BIN" ]]; then
    printf '%s\n' "${UNKNOWN} \"PM2\" - Unable to locate PM2 executable"
    exit 0
fi

PM2_BIN_DIR="$(dirname "$PM2_BIN")"

#
# Node used by this PM2 installation.
#

NODE_BIN="${PM2_BIN_DIR}/node"

if [[ ! -x "$NODE_BIN" ]]; then
    NODE_BIN="$(command -v node 2>/dev/null || true)"
fi

#
# ---------------------------------------------------------------------------
# Retrieve PM2 process list
# ---------------------------------------------------------------------------
#

if [[ "$PM2_USER" == "root" ]]; then

    PM2_JSON="$(
        PM2_HOME="$PM2_HOME" \
        PATH="${PM2_BIN_DIR}:${PATH}" \
        "$PM2_BIN" jlist 2>/dev/null
    )"

    PM2_RC=$?

else

    PM2_JSON="$(
        runuser -u "$PM2_USER" -- \
            env \
            PM2_HOME="$PM2_HOME" \
            PATH="${PM2_BIN_DIR}:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" \
            "$PM2_BIN" jlist 2>/dev/null
    )"

    PM2_RC=$?

fi

if [[ $PM2_RC -ne 0 || -z "$PM2_JSON" ]]; then
    printf '%s\n' "${CRIT} \"PM2\" - Unable to retrieve PM2 process list for user ${PM2_USER}"
    exit 0
fi

if ! printf '%s' "$PM2_JSON" | jq -e 'type == "array"' >/dev/null 2>&1; then
    printf '%s\n' "${UNKNOWN} \"PM2\" - PM2 returned invalid JSON"
    exit 0
fi

PROCESS_COUNT="$(
    printf '%s' "$PM2_JSON" |
        jq 'length'
)"

if [[ "$PROCESS_COUNT" -eq 0 ]]; then
    printf '%s\n' "${WARN} \"PM2\" processes=0 No processes managed by PM2 for user ${PM2_USER}"
    exit 0
fi

#
# ---------------------------------------------------------------------------
# Overall PM2 state
# ---------------------------------------------------------------------------
#

ONLINE_COUNT="$(
    printf '%s' "$PM2_JSON" |
        jq '[.[] | select(.pm2_env.status == "online")] | length'
)"

OFFLINE_COUNT=$((PROCESS_COUNT - ONLINE_COUNT))

if [[ "$OFFLINE_COUNT" -eq 0 ]]; then
    printf '%s\n' \
        "${OK} \"PM2\" processes=${PROCESS_COUNT};;;0|online=${ONLINE_COUNT};;;0|offline=0;;;0 ${PROCESS_COUNT}/${PROCESS_COUNT} processes online"
else
    printf '%s\n' \
        "${CRIT} \"PM2\" processes=${PROCESS_COUNT};;;0|online=${ONLINE_COUNT};;;0|offline=${OFFLINE_COUNT};;;0 ${ONLINE_COUNT}/${PROCESS_COUNT} processes online, ${OFFLINE_COUNT} not online"
fi

#
# ---------------------------------------------------------------------------
# PM2 version / update
# ---------------------------------------------------------------------------
#

PM2_VERSION="$(
    PATH="${PM2_BIN_DIR}:${PATH}" \
    "$PM2_BIN" --version 2>/dev/null |
        tail -n 1 |
        tr -d '[:space:]'
)"

if [[ "$CHECK_PM2_UPDATES" == "yes" ]]; then

    if [[ -z "$PM2_VERSION" ]]; then
        printf '%s\n' "${UNKNOWN} \"PM2 Update\" - Unable to determine installed PM2 version"

    else

        PM2_RELEASE_RESPONSE="$(
            curl \
                --silent \
                --show-error \
                --fail \
                --connect-timeout 3 \
                --max-time 8 \
                "$PM2_PACKAGE_URL" \
                2>/dev/null
        )"

        CURL_RC=$?

        if [[ $CURL_RC -ne 0 || -z "$PM2_RELEASE_RESPONSE" ]]; then
            printf '%s\n' \
                "${UNKNOWN} \"PM2 Update\" - Installed ${PM2_VERSION}, unable to query npm registry"

        elif ! printf '%s' "$PM2_RELEASE_RESPONSE" | jq -e . >/dev/null 2>&1; then
            printf '%s\n' \
                "${UNKNOWN} \"PM2 Update\" - npm registry returned invalid JSON"

        else

            PM2_LATEST="$(
                printf '%s' "$PM2_RELEASE_RESPONSE" |
                    jq -r '.version // empty'
            )"

            if [[ -z "$PM2_LATEST" ]]; then
                printf '%s\n' \
                    "${UNKNOWN} \"PM2 Update\" - Unable to determine latest PM2 version"

            else

                NEWEST="$(
                    printf '%s\n%s\n' \
                        "$PM2_VERSION" \
                        "$PM2_LATEST" |
                        sort -V |
                        tail -n 1
                )"

                if [[ "$PM2_VERSION" == "$PM2_LATEST" ]]; then
                    printf '%s\n' \
                        "${OK} \"PM2 Update\" - Current: installed ${PM2_VERSION}, latest ${PM2_LATEST}"

                elif [[ "$NEWEST" == "$PM2_LATEST" ]]; then
                    printf '%s\n' \
                        "${WARN} \"PM2 Update\" - Update available: installed ${PM2_VERSION}, latest ${PM2_LATEST}"

                else
                    printf '%s\n' \
                        "${OK} \"PM2 Update\" - Installed ${PM2_VERSION} is newer than latest ${PM2_LATEST}"
                fi
            fi
        fi
    fi
fi

#
# ---------------------------------------------------------------------------
# Node.js / NVM version
# ---------------------------------------------------------------------------
#

if [[ "$CHECK_NODE_UPDATES" == "yes" ]]; then

    if [[ -z "$NODE_BIN" || ! -x "$NODE_BIN" ]]; then
        printf '%s\n' "${UNKNOWN} \"Node.js Update\" - Unable to locate Node.js executable"

    else

        NODE_VERSION="$(
            "$NODE_BIN" --version 2>/dev/null |
                sed 's/^v//'
        )"

        if [[ -z "$NODE_VERSION" ]]; then
            printf '%s\n' "${UNKNOWN} \"Node.js Update\" - Unable to determine installed Node.js version"

        else

            NODE_RELEASES="$(
                curl \
                    --silent \
                    --show-error \
                    --fail \
                    --connect-timeout 3 \
                    --max-time 8 \
                    "$NODE_RELEASES_URL" \
                    2>/dev/null
            )"

            CURL_RC=$?

            if [[ $CURL_RC -ne 0 || -z "$NODE_RELEASES" ]]; then
                printf '%s\n' \
                    "${UNKNOWN} \"Node.js Update\" - Installed ${NODE_VERSION}, unable to query Node.js releases"

            elif ! printf '%s' "$NODE_RELEASES" | jq -e 'type == "array"' >/dev/null 2>&1; then
                printf '%s\n' \
                    "${UNKNOWN} \"Node.js Update\" - Node.js release API returned invalid data"

            else

                INSTALLED_MAJOR="${NODE_VERSION%%.*}"

                LATEST_CURRENT="$(
                    printf '%s' "$NODE_RELEASES" |
                        jq -r '
                            [
                                .[]
                                | select(.lts == false)
                                | .version
                            ][0] // empty
                        ' |
                        sed 's/^v//'
                )"

                LATEST_LTS="$(
                    printf '%s' "$NODE_RELEASES" |
                        jq -r '
                            [
                                .[]
                                | select(.lts != false)
                                | .version
                            ][0] // empty
                        ' |
                        sed 's/^v//'
                )"

                LATEST_SAME_MAJOR="$(
                    printf '%s' "$NODE_RELEASES" |
                        jq -r \
                            --arg prefix "v${INSTALLED_MAJOR}." '
                            [
                                .[]
                                | select(.version | startswith($prefix))
                                | .version
                            ][0] // empty
                        ' |
                        sed 's/^v//'
                )"

                SAME_MAJOR_LTS="$(
                    printf '%s' "$NODE_RELEASES" |
                        jq -r \
                            --arg prefix "v${INSTALLED_MAJOR}." '
                            [
                                .[]
                                | select(.version | startswith($prefix))
                                | .lts
                            ][0] // false
                        '
                )"

                #
                # If no releases for the installed major exist in the current
                # Node.js release index, consider it unsupported/EOL.
                #

                if [[ -z "$LATEST_SAME_MAJOR" ]]; then

                    printf '%s\n' \
                        "${WARN} \"Node.js Update\" - Installed ${NODE_VERSION} appears unsupported/EOL; latest LTS ${LATEST_LTS}, current ${LATEST_CURRENT}"

                elif [[ "$SAME_MAJOR_LTS" == "false" && "$INSTALLED_MAJOR" -lt "${LATEST_CURRENT%%.*}" ]]; then

                    printf '%s\n' \
                        "${WARN} \"Node.js Update\" - Installed ${NODE_VERSION} is on a non-LTS release line; latest LTS ${LATEST_LTS}, current ${LATEST_CURRENT}"

                elif [[ "$NODE_VERSION" != "$LATEST_SAME_MAJOR" ]]; then

                    NEWEST="$(
                        printf '%s\n%s\n' \
                            "$NODE_VERSION" \
                            "$LATEST_SAME_MAJOR" |
                            sort -V |
                            tail -n 1
                    )"

                    if [[ "$NEWEST" == "$LATEST_SAME_MAJOR" ]]; then
                        printf '%s\n' \
                            "${WARN} \"Node.js Update\" - Update available in Node ${INSTALLED_MAJOR}.x: installed ${NODE_VERSION}, latest ${LATEST_SAME_MAJOR}; latest LTS ${LATEST_LTS}"
                    else
                        printf '%s\n' \
                            "${OK} \"Node.js Update\" - Installed ${NODE_VERSION}; latest LTS ${LATEST_LTS}, current ${LATEST_CURRENT}"
                    fi

                else

                    printf '%s\n' \
                        "${OK} \"Node.js Update\" - Current on Node ${INSTALLED_MAJOR}.x: ${NODE_VERSION}; latest LTS ${LATEST_LTS}, current ${LATEST_CURRENT}"
                fi
            fi
        fi
    fi
fi

#
# ---------------------------------------------------------------------------
# Individual PM2 processes
# ---------------------------------------------------------------------------
#

while IFS= read -r PROCESS; do

    NAME="$(
        printf '%s' "$PROCESS" |
            jq -r '.name // "unknown"'
    )"

    STATUS="$(
        printf '%s' "$PROCESS" |
            jq -r '.pm2_env.status // "unknown"'
    )"

    PID="$(
        printf '%s' "$PROCESS" |
            jq -r '.pid // 0'
    )"

    RESTARTS="$(
        printf '%s' "$PROCESS" |
            jq -r '.pm2_env.restart_time // 0'
    )"

    CRON_RESTART="$(
        printf '%s' "$PROCESS" |
            jq -r '.pm2_env.cron_restart // empty'
    )"

    AUTORESTART="$(
        printf '%s' "$PROCESS" |
            jq -r '.pm2_env.autorestart // true'
    )"

    CPU="$(
        printf '%s' "$PROCESS" |
            jq -r '.monit.cpu // 0'
    )"

    MEMORY_BYTES="$(
        printf '%s' "$PROCESS" |
            jq -r '.monit.memory // 0'
    )"

    START_TIME="$(
        printf '%s' "$PROCESS" |
            jq -r '.pm2_env.pm_uptime // 0'
    )"

    NAME="${NAME//\"/\'}"

    MEMORY_MB="$(
        awk -v bytes="$MEMORY_BYTES" \
            'BEGIN { printf "%.1f", bytes / 1024 / 1024 }'
    )"

    NOW_MS=$(( $(date +%s) * 1000 ))

    if [[ "$START_TIME" =~ ^[0-9]+$ && "$START_TIME" -gt 0 ]]; then

        UPTIME_SECONDS="$(
            awk -v now="$NOW_MS" -v start="$START_TIME" \
                'BEGIN {
                    diff = (now - start) / 1000
                    if (diff < 0) diff = 0
                    printf "%.0f", diff
                }'
        )"

    else
        UPTIME_SECONDS=0
    fi

    DAYS=$((UPTIME_SECONDS / 86400))
    HOURS=$(((UPTIME_SECONDS % 86400) / 3600))
    MINUTES=$(((UPTIME_SECONDS % 3600) / 60))

    if [[ "$DAYS" -gt 0 ]]; then
        UPTIME_TEXT="${DAYS}d ${HOURS}h ${MINUTES}m"
    elif [[ "$HOURS" -gt 0 ]]; then
        UPTIME_TEXT="${HOURS}h ${MINUTES}m"
    else
        UPTIME_TEXT="${MINUTES}m"
    fi

    STATE=$OK
    STATE_TEXT="Online"

    if [[ "$STATUS" != "online" ]]; then
        STATE=$CRIT
        STATE_TEXT="Status ${STATUS}"
    fi

    if [[ -n "$CRON_RESTART" ]]; then
        RESTART_INFO="restarts ${RESTARTS}, scheduled restart ${CRON_RESTART}"
    else
        RESTART_INFO="restarts ${RESTARTS}"
    fi

    if [[ "$AUTORESTART" == "false" ]]; then
        RESTART_INFO="${RESTART_INFO}, autorestart disabled"
    fi

    PERFDATA="cpu=${CPU}%;;;0;100|memory=${MEMORY_BYTES}B;;;0|restarts=${RESTARTS};;;0|uptime=${UPTIME_SECONDS}s;;;0"

    printf '%s\n' \
        "${STATE} \"PM2 ${NAME}\" ${PERFDATA} ${STATE_TEXT} - PID ${PID}, uptime ${UPTIME_TEXT}, CPU ${CPU}%, memory ${MEMORY_MB} MiB, ${RESTART_INFO}"

done < <(
    printf '%s' "$PM2_JSON" |
        jq -c '.[]'
)

exit 0